﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using TimelineNet.Library;
using System.Web.UI.HtmlControls;

namespace TimelineNet {

    public enum DataSourceTypes { rss, named, localxml, remotexml/*, remotevcs*/ };
    public enum ScriptSourceLocations { Local, Remote };
    [DefaultProperty("Text")]
    [ToolboxData("<{0}:Timeline Height=\"300\" Width=\"100%\" runat=server></{0}:Timeline>")]
    [Serializable()]
    public class Timeline : WebControl, INamingContainer {

        protected override void RenderContents(HtmlTextWriter output) {

            if(isSafeToRender) {

                if(EnableSearchAndFilter) {
                    this.Page.ClientScript.RegisterClientScriptResource(resType, "TimelineNet.js.timeline.timelineSearchFilter.js");
                    this.ScriptSourceLocation = ScriptSourceLocations.Remote;
                }


                Library.Timeline t = new Library.Timeline();
                t.DataSource = this.TimelineData;

                if(t.DataSource == null) {
                    t.DataSource = new TimelineData();
                    t.DataSource.Add(new Event(DateTime.Now, DateTime.Now, false, "No Data Provided!", "", "You must provide data to the Timeline Control.", ""));
                }

                DefaultEventSource evtSource = new DefaultEventSource();
                evtSource.Name = "eventSource";
                if(t.EventSources == null) t.EventSources = new System.Collections.Generic.List<EventSource>();
                t.EventSources.Add(evtSource);

                BuildTheme(t);
                BuildBands(t, evtSource);

                BuildClientElements(t);
                BuildData(t);

                output.Write(BuildOutput(t));


            } else {
                output.Write("Timeline could not find the head tag. Be sure you have one on the page: &lt;head runat=\"server\"&gt;");
            }
            base.RenderContents(output);

        }

        private bool isSafeToRender = false;
        protected override void OnInit(EventArgs e) {
            base.OnInit(e);
            this.Page.ClientScript.RegisterClientScriptResource(resType, "TimelineNet.js.timeline.InitTimeline.js");
            string sandfUrl = this.Page.ClientScript.GetWebResourceUrl(resType, "TimelineNet.js.timeline.timelineSearchFilter.js");
            
            HtmlHead head = null;
            if(Page.Master != null) {
                foreach(Control c in Page.Master.Controls) {
                    head = FindTheHeadTag(c);
                    if(head != null) break;
                }
            }
            if(head == null) {
                foreach(Control c in Page.Controls) {
                    head = FindTheHeadTag(c);
                    if(head != null) break;
                }
            }
            if(head != null) {
                bool found = false;
                foreach(Control c in head.Controls) {
                    if((c as LiteralControl) != null) {
                        LiteralControl ctl = (c as LiteralControl);
                        if(ctl.Text.Contains("timeline-api.js")) found = true;
                    }
                }
                if(!found) {
                    if(this.ScriptSourceLocation == ScriptSourceLocations.Remote) {
                        head.Controls.Add(new LiteralControl("<script src=\"http://simile.mit.edu/timeline/api/timeline-api.js\" type=\"text/javascript\"></script>"));
                    } else {
                        head.Controls.Add(new LiteralControl(string.Format("<script src=\"{0}api/timeline-api.js?i=0&timeline-use-local-resources=true\" type=\"text/javascript\"></script>",  ResolveUrl(LocalTimelineJSFolder))));
                    }
                    if(EnableSearchAndFilter) head.Controls.Add(new LiteralControl(string.Format("<script src=\"{0}\" type=\"text/javascript\"></script>", sandfUrl)));
                }
                isSafeToRender = true;
            }
        }


        #region Builder Methods
        private void BuildTheme(Library.Timeline t) {
            if(Theme != null) t.Theme = Theme;

            if(this.LabelWidth > 0 || this.BubbleHeight > 0 || this.BubbleWidth > 0) {
                if(t.Theme == null) t.Theme = new Library.Themes.ClassicTheme();
                t.Theme.ThemeEvent = new Library.Themes.ThemeEvent();
                if(this.LabelWidth > 0) {
                    t.Theme.ThemeEvent.ThemeEventLabel = new Library.Themes.ThemeEventLabel();
                    t.Theme.ThemeEvent.ThemeEventLabel.width = LabelWidth;
                }
                if(this.BubbleHeight > 0 || this.BubbleWidth > 0) {
                    t.Theme.ThemeEvent.ThemEventBubble = new Library.Themes.ThemeEventBubble();
                    t.Theme.ThemeEvent.ThemEventBubble.width = BubbleWidth;
                    t.Theme.ThemeEvent.ThemEventBubble.height = BubbleHeight;
                }
            }
        }

        private void BuildBands(Library.Timeline t, EventSource evtSource) {

            if(this.Bands.Count == 0) {
                HotZoneBand YEAR = new HotZoneBand(10, Enums.TimelineDateTime.YEAR, 100);
                HotZoneBand MONTH = new HotZoneBand(10, Enums.TimelineDateTime.MONTH, 100);
                HotZoneBand DAY = new HotZoneBand(80, Enums.TimelineDateTime.DAY, 100);

                YEAR.Overview = true;
                MONTH.Overview = true;
                YEAR.TrackGap = new decimal(0.1);
                YEAR.TrackHeight = new decimal(0.2);
                MONTH.TrackGap = new decimal(0.5);
                MONTH.TrackHeight = new decimal(0.2);

                

                MONTH.SynchWith = 0;
                YEAR.SynchWith = 0;
                this.Bands.Add(DAY);
                this.Bands.Add(MONTH);
                this.Bands.Add(YEAR);
            }

            foreach(Band b in this.Bands) {
                b.EventSource = t.EventSources[0];
                t.Add(b);
            }

        }
        string aSHXPath;
        private string ASHXPath
        {
            get
            {
                if(aSHXPath == null)
                {
                    aSHXPath = System.IO.Path.Combine(Page.Request.ApplicationPath, "TimelineData.ashx");
                    aSHXPath = aSHXPath.Replace(@"\", "/");
                }
                return aSHXPath;
            }
        }
        private void BuildData(Library.Timeline t) {

            if(DataSourceType == DataSourceTypes.named) {
                t.XMLDocument = string.Format("{2}?DataID={0}&type={1}", t.DataSource.DataID, this.DataSourceType.ToString(), ASHXPath);
            } else {
                t.DataSource = new TimelineData();
                t.DataSource.DataUrl = this.DataUrl;
            }
            t.XMLDocument = string.Format("{2}?DataID={0}&type={1}", t.DataSource.DataID, this.DataSourceType.ToString(), ASHXPath);
            Page.Cache.Add(t.DataSource.DataID, t.DataSource, null, DateTime.Now.AddMinutes(1), System.Web.Caching.Cache.NoSlidingExpiration, System.Web.Caching.CacheItemPriority.Default, null);

        }
        private string BuildOutput(Library.Timeline t) {

            string sandf = "";
            if(EnableSearchAndFilter) {
                //setupFilterHighlightControls(document.getElementById("my-timeline-controls"), tl, [0,1], theme);
                sandf = string.Format("setupFilterHighlightControls(document.getElementById(\"{0}\"), {1}, [0,1], {2});\r\n", SearchAndFilterTag, t.TimelineVarName, this.Theme.ThemeClientID);
            }


            System.Text.StringBuilder script = new StringBuilder();
            script.Append(string.Format("<script type=\"text/javascript\">function {0}(){{}}</script> \r\n", t.TimelineVarName));
            script.Append("<div id=\"" + ParentElementID + "\" style=\"height: " + this.Height + "; border: 1px solid #aaa\"></div>");
            script.Append(string.Format("<script type=\"text/javascript\">{0}.onLoad = function() \r\n", t.TimelineVarName));
            script.Append("{\r\n");
            script.Append(string.Format("{0}\r\n", t.RenderTimelines()));
            script.Append(string.Format("\r\n{0}\r\n", sandf));
            script.Append("};\r\n");
            script.Append(string.Format("init('{0}.onLoad();');\r\n", t.TimelineVarName));
            script.Append("</script>");


            return script.ToString();
        }
        private void BuildClientElements(Library.Timeline t) {
            if(this.Height.Value < 50) this.Height = new Unit(50);
            t.ParentElementID = ParentElementID;
            t.TimelineVarName = this.JSName;
            t.Vertical = this.Vertical;
        }
        #endregion


        protected HtmlHead FindTheHeadTag(Control C) {
            if((C as HtmlHead != null)) return (C as HtmlHead);
            foreach(Control c in C.Controls) {
                if((c as HtmlHead != null)) return (c as HtmlHead);
                return FindTheHeadTag(c);
            }
            return null;
        }


        Type resType = typeof(Timeline);

        #region Local Properties

        /// <summary>
        /// The name of the variable in javascript to represent our timeline.
        /// This should be set to a unique value per instance
        /// </summary>
        [Bindable(true)]
        [Category("Appearance")]
        [DefaultValue("300")]
        [Localizable(false)]
        public string JSName {
            get {
                return PropertyFromViewState<string>("JSName", "t1");
            }
            set {
                ViewState.Add("JSName", value);
            }
        }

        /// <summary>
        /// The width of the label
        /// </summary>
        [Bindable(true)]
        [Category("Appearance")]
        [DefaultValue("300")]
        [Localizable(false)]
        public int LabelWidth {
            get {
                return PropertyFromViewState<int>("LabelWidth", 300);
            }
            set {
                ViewState.Add("LabelWidth", value);
            }
        }

        /// <summary>
        /// The typical width of the bubble
        /// </summary>
        [Bindable(true)]
        [Category("Appearance")]
        [DefaultValue("300")]
        [Localizable(true)]
        public int BubbleWidth {
            get {
                return PropertyFromViewState<int>("BubbleWidth", 300);
            }
            set {
                ViewState.Add("BubbleWidth", value);
            }
        }

        /// <summary>
        /// The typical height of the bubble
        /// </summary>
        [Bindable(true)]
        [Category("Appearance")]
        [DefaultValue("300")]
        [Localizable(true)]
        public int BubbleHeight {
            get {
                return PropertyFromViewState<int>("BubbleHeight", 300);
            }
            set {
                ViewState.Add("BubbleHeight", value);
            }
        }

        /// <summary>
        /// The client side element to render the timeline into
        /// </summary>
        [Bindable(false)]
        [Category("Appearance")]
        [Localizable(true)]
        public string ParentElementID {
            get {
                return PropertyFromViewState<string>("ParentElementID", "my-timeline");
            }
            set {
                ViewState.Add("ParentElementID", value);
            }
        }

        /// <summary>
        /// If you set the ScriptSourceLocation variable to local, you can set the root path to the timeline folder.
        /// Typically this will be the relative path from the root of the current applicaiton which contains the timeline-api.js file
        /// </summary>
        [Bindable(false)]
        [Category("Appearance")]
        [Localizable(true)]
        public string LocalTimelineJSFolder {
            get {
                return PropertyFromViewState<string>("LocalTimelineJSFolder", ResolveUrl("~/js/timeline/"));
            }
            set {
                ViewState.Add("LocalTimelineJSFolder", value);
            }
        }
      
        /// <summary>
        /// Allows you to choose remote vs local script source
        /// Remote location defaults to: http://simile.mit.edu/timeline/api/timeline-api.js
        /// </summary>
        [Bindable(false)]
        [Category("Appearance")]
        [Localizable(true)]
        public ScriptSourceLocations ScriptSourceLocation {
            get {
                return PropertyFromViewState<ScriptSourceLocations>("ScriptSourceLocation", ScriptSourceLocations.Remote);
            }
            set {
                ViewState.Add("ScriptSourceLocation", value);
            }
        }

        /// <summary>
        /// The root of our data source to add events to.
        /// This is your starting point for adding data.
        /// </summary>
        [Bindable(true)]
        [Category("Data")]
        [Localizable(true)]
        public TimelineData TimelineData {
            get {
                return PropertyFromSession<TimelineData>("TimelineData" + this.ClientID);
            }
            set {
                System.Web.HttpContext.Current.Session.Add("TimelineData" + this.ClientID, value);
            }
        }

        /// <summary>
        /// The theme which will be used to render the timeline
        /// Default is the ClassicTheme
        /// </summary>
        [Bindable(true)]
        [Category("Appearance")]
        [Localizable(true)]
        public Theme Theme {
            get {
                return PropertyFromViewState<Theme>("Theme", new Library.Themes.ClassicTheme());
            }
            set {
                ViewState.Add("Theme", value);
            }
        }


        /// <summary>
        /// http://simile.mit.edu/issues/browse/TIMELINE-8  Vertical timelines are not supported
        /// </summary>
        [Bindable(true)]
        [Category("Appearance")]
        [Localizable(true)]
        private bool Vertical {
            get {
                return PropertyFromViewState<bool>("Vertical", false);
            }
            set {
                ViewState.Add("Vertical", value);
            }
        }

        /// <summary>
        /// Allows you to control the Search and Filter controls
        /// Note: If this is enabled it will force the script source location to remote
        /// </summary>
        [Bindable(true)]
        [Category("Appearance")]
        [Localizable(true)]
        public bool EnableSearchAndFilter {
            get {
                return PropertyFromViewState<bool>("EnableSearchAndFilter", false);
            }
            set {
                ViewState.Add("EnableSearchAndFilter", value);
            }
        }

        /// <summary>
        /// The type of data source.
        /// </summary>
        [Bindable(true)]
        [Category("Data")]
        [Localizable(true)]
        public DataSourceTypes DataSourceType {
            get {
                return PropertyFromViewState<DataSourceTypes>("DataSourceType", DataSourceTypes.named);
            }
            set {
                ViewState.Add("DataSourceType", value);
            }
        }

        /// <summary>
        /// A list of the bands to render.
        /// This is your starting point for adding bands of different types and time ranges.
        /// A zone is added to the specific band itself.
        /// </summary>
        [Bindable(true)]
        [Category("Appearance")]
        [Localizable(true)]
        public List<Band> Bands {
            get {
                return PropertyFromViewState<List<Band>>("Bands", new List<Band>());
            }
            set {
                ViewState.Add("Bands", value);
            }
        }
        
        /// <summary>
        /// If you are using a remote or local data source (other than named) you must provide the URL to the resource to load
        /// </summary>
        [Bindable(true)]
        [Category("Data")]
        [Localizable(true)]
        public string DataUrl {
            get {
                return PropertyFromViewState<string>("DataUrl");
            }
            set {
                ViewState.Add("DataUrl", value);
            }
        }

        /// <summary>
        /// The client side DOM element which to render the Search and Filter elements
        /// </summary>
        [Bindable(true)]
        [Category("Appearance")]
        [Localizable(true)]
        public string SearchAndFilterTag {
            get {
                return PropertyFromViewState<string>("SearchAndFilterTag", this.ParentElementID);
            }
            set {
                ViewState.Add("SearchAndFilterTag", value);
            }
        }
        #endregion

        #region viewstate/session helpers
        private T PropertyFromViewState<T>(string Name) {
            object val = ViewState[Name];
            if(val != null) return (T)val;
            T obj = default(T);
            ViewState.Add(Name, obj);
            return obj;
        }
        private T PropertyFromViewState<T>(string Name, T DefaultValue) {
            object val = ViewState[Name];
            if(val != null) return (T)val;
            ViewState.Add(Name, DefaultValue);
            return DefaultValue;
        }
        private T PropertyFromSession<T>(string Name) {
            object val = System.Web.HttpContext.Current.Session[Name];
            if(val != null) return (T)val;
            T obj = default(T);
            System.Web.HttpContext.Current.Session.Add(Name, obj);
            return obj;
        }
        private T PropertyFromSession<T>(string Name, T DefaultValue) {
            object val = System.Web.HttpContext.Current.Session[Name];
            if(val != null) return (T)val;
            System.Web.HttpContext.Current.Session.Add(Name, DefaultValue);
            return DefaultValue;
        }
        #endregion


        #region helpers
        /// <summary>

        /// Returns a site relative HTTP path from a partial path starting out with a ~.

        /// Same syntax that ASP.Net internally supports but this method can be used

        /// outside of the Page framework.

        /// 

        /// Works like Control.ResolveUrl including support for ~ syntax

        /// but returns an absolute URL.

        /// </summary>

        /// <param name="originalUrl">Any Url including those starting with ~</param>

        /// <returns>relative url</returns>

        public static string ResolveUrl(string originalUrl)
        {

            if (originalUrl == null)

                return null;



            // *** Absolute path - just return

            if (originalUrl.IndexOf("://") != -1)

                return originalUrl;



            // *** Fix up image path for ~ root app dir directory

            if (originalUrl.StartsWith("~"))
            {

                string newUrl = "";

                if (HttpContext.Current != null)

                    newUrl = HttpContext.Current.Request.ApplicationPath +

                          originalUrl.Substring(1).Replace("//", "/");

                else

                    // *** Not context: assume current directory is the base directory

                    throw new ArgumentException("Invalid URL: Relative URL not allowed.");



                // *** Just to be sure fix up any double slashes

                return newUrl;

            }



            return originalUrl;

        }
        #endregion
    }
}